﻿// <copyright file="PagedCollection.cs" company="Ucaya">
// </copyright>
// <author>Ucaya</author>

namespace Channel9Kinect
{
    #region

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Collections.Specialized;
    using System.Linq;

    using Channel9Kinect.Core;

    #endregion

    public interface IPagedCollection
    {
        #region Public Events

        event EventHandler<EventArgs> PageChanged;

        event EventHandler<EventArgs> PageChangedCancelled;

        event EventHandler<DataEventArgs<Exception>> PageChangedException;

        event EventHandler<PageChangingEventArgs> PageChanging;

        #endregion

        #region Public Properties

        bool CanMoveNext { get; }

        bool CanMovePrevious { get; }

        bool IsPageChanging { get; }

        IEnumerable Page { get; }

        int PageIndex { get; }

        int PageSize { get; set; }

        int TotalItemCount { get; set; }

        #endregion

        #region Public Methods

        bool MoveToFirstPage();

        bool MoveToNextPage();

        bool MoveToPage(int pageIndex);

        bool MoveToPreviousPage();

        #endregion
    }

    public interface IPagedCollection<T> : IPagedCollection
    {
        #region Public Properties

        new IEnumerable<T> Page { get; }

        #endregion
    }

    public class PagedCollection<T> : ViewModelBase, IPagedCollection<T>, IEnumerable<T>, INotifyCollectionChanged
    {
        #region Constants and Fields

        private readonly Action<PagedCollectionRequest> asyncLoadPage;

        private readonly Action<PagedCollectionRequest> cancelAsyncLoadPage;

        private bool canChangePage;

        private bool isPageChanging;

        private int itemCount;

        private IEnumerable<T> page;

        private int pageIndex;

        private int pageSize;

        private PagedCollectionRequest pendingRequest;

        private int totalItemCount;

        #endregion

        #region Constructors and Destructors

        public PagedCollection(int totalItemCount, Action<PagedCollectionRequest> asyncLoadPage, IEnumerable<T> firstPage = null, int? pageSize = null, Action<PagedCollectionRequest> cancelAsyncLoadPage = null)
        {
            if (asyncLoadPage == null)
            {
                throw new ArgumentNullException("asyncLoadPage");
            }

            this.asyncLoadPage = asyncLoadPage;
            this.cancelAsyncLoadPage = cancelAsyncLoadPage;
            this.TotalItemCount = totalItemCount;
            this.pageIndex = 0;

            List<T> page = firstPage != null ? firstPage.ToList() : null;
            this.PageSize = pageSize != null ? pageSize.Value : page != null ? page.Count : 0;
            this.Page = page;
            this.CanChangePage = true;
        }

        #endregion

        #region Public Events

        public event EventHandler<EventArgs> PageChanged;

        public event EventHandler<EventArgs> PageChangedCancelled;

        public event EventHandler<DataEventArgs<Exception>> PageChangedException;

        public event EventHandler<PageChangingEventArgs> PageChanging;

        #endregion

        #region Explicit Interface Events

        event NotifyCollectionChangedEventHandler INotifyCollectionChanged.CollectionChanged
        {
            add
            {
                this.collectionChanged += value;
            }
            remove
            {
                this.collectionChanged -= value;
            }
        }

        #endregion

        #region Events

        private event NotifyCollectionChangedEventHandler collectionChanged;

        #endregion

        #region Public Properties

        public bool AllowMultiplesRequests { get; set; }

        public bool CanChangePage
        {
            get
            {
                return this.canChangePage;
            }
            set
            {
                if (this.canChangePage != value)
                {
                    this.canChangePage = value;
                    this.RaisePropertyChanged(n => n.CanChangePage);
                }
            }
        }

        public bool CanMoveNext
        {
            get
            {
                return this.PageIndex < this.PageCount - 1;
            }
        }

        public bool CanMovePrevious
        {
            get
            {
                return this.PageIndex > 0;
            }
        }

        public bool IsPageChanging
        {
            get
            {
                return this.isPageChanging;
            }
            private set
            {
                if (this.isPageChanging != value)
                {
                    this.isPageChanging = value;
                    this.RaisePropertyChanged(n => n.IsPageChanging);
                }
            }
        }

        public int ItemCount
        {
            get
            {
                return this.itemCount;
            }
            private set
            {
                if (this.itemCount != value)
                {
                    this.itemCount = value;
                    this.RaisePropertyChanged(n => n.ItemCount);
                }
            }
        }

        public IEnumerable<T> Page
        {
            get
            {
                return this.page;
            }
            set
            {
                if (this.page != value)
                {
                    this.page = value;
                    this.RaisePropertyChanged(n => n.Page);
                }
            }
        }

        public int PageCount
        {
            get
            {
                return (int)Math.Ceiling((double)this.TotalItemCount / this.PageSize);
            }
        }

        public int PageIndex
        {
            get
            {
                return this.pageIndex;
            }
            private set
            {
                if (this.pageIndex != value)
                {
                    this.pageIndex = value;
                    this.RaisePropertyChanged(n => n.PageIndex);
                    this.RaisePropertyChanged(n => n.CanMoveNext);
                    this.RaisePropertyChanged(n => n.CanMovePrevious);
                }
            }
        }

        public int PageSize
        {
            get
            {
                return this.pageSize;
            }
            set
            {
                if (this.pageSize != value)
                {
                    int itemPosition = this.pageSize * this.pageIndex;
                    this.pageSize = value;
                    this.RaisePropertyChanged(n => n.PageSize);
                    this.RaisePropertyChanged(n => n.PageCount);
                    this.RaisePropertyChanged(n => n.CanMoveNext);
                    this.RaisePropertyChanged(n => n.CanMovePrevious);
                    if (!this.IsPageChanging && this.PageSize != 0)
                    {
                        var pageIndex = (int)Math.Floor((double)itemPosition / this.PageSize);
                        this.MoveToPage(pageIndex);
                    }
                }
            }
        }

        public int TotalItemCount
        {
            get
            {
                return this.totalItemCount;
            }
            set
            {
                if (this.totalItemCount != value)
                {
                    this.totalItemCount = value;
                    this.RaisePropertyChanged(n => n.TotalItemCount);
                    this.RaisePropertyChanged(n => n.PageCount);
                    this.RaisePropertyChanged(n => n.CanMoveNext);
                    this.RaisePropertyChanged(n => n.CanMovePrevious);
                }
            }
        }

        #endregion

        #region Explicit Interface Properties

        IEnumerable IPagedCollection.Page
        {
            get
            {
                return this.Page;
            }
        }

        #endregion

        #region Public Methods

        public bool MoveToFirstPage()
        {
            return this.MoveToPage(0);
        }

        public bool MoveToLastPage()
        {
            return this.MoveToPage(this.PageCount - 1);
        }

        public bool MoveToNextPage()
        {
            if (this.CanMoveNext)
            {
                return this.MoveToPage(this.PageIndex + 1);
            }

            return false;
        }

        public bool MoveToPage(int pageIndex)
        {
            if (!this.CanChangePage)
            {
                return false;
            }

            if (!this.AllowMultiplesRequests && this.IsPageChanging)
            {
                return false;
            }

            if (!this.RaisePageChanging(pageIndex))
            {
                return false;
            }
            this.IsPageChanging = true;

            // Cancel pendingRequest
            if (this.pendingRequest != null && this.cancelAsyncLoadPage != null)
            {
                this.cancelAsyncLoadPage(this.pendingRequest);
            }

            // Create new request
            this.pendingRequest = new PagedCollectionRequest(pageIndex * this.PageSize, this.PageSize, this, (result, error, cancelled) =>
                {
                    bool success = false;
                    try
                    {
                        if (error != null)
                        {
                            if (this.PageChangedException != null)
                            {
                                this.PageChangedException(this, DataEventArgs.Create(new Exception("Failed to change page", error)));
                            }
                            return;
                        }
                        if (cancelled)
                        {
                            this.RaisePageChangedCancelled();
                        }
                        else
                        {
                            success = true;
                            this.Page = result != null ? result.ToList() : null;
                            this.PageIndex = pageIndex;
                        }
                    }
                    finally
                    {
                        this.IsPageChanging = false;
                        this.pendingRequest = null;
                    }

                    if (success)
                    {
                        this.RaiseCollectionChanged();
                        this.RaisePageChanged();
                    }
                });

            this.asyncLoadPage(this.pendingRequest);

            return true;
        }

        public bool MoveToPreviousPage()
        {
            if (this.CanMovePrevious)
            {
                return this.MoveToPage(this.PageIndex - 1);
            }
            return false;
        }

        #endregion

        #region Explicit Interface Methods

        IEnumerator IEnumerable.GetEnumerator()
        {
            return ((IEnumerable<T>)this).GetEnumerator();
        }

        IEnumerator<T> IEnumerable<T>.GetEnumerator()
        {
            if (this.Page != null)
            {
                return this.Page.GetEnumerator();
            }

            return new List<T>().GetEnumerator();
        }

        #endregion

        #region Methods

        private void RaiseCollectionChanged()
        {
            if (this.collectionChanged != null)
            {
                this.collectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
            }
        }

        private void RaisePageChanged()
        {
            if (this.PageChanged != null)
            {
                this.PageChanged(this, EventArgs.Empty);
            }
        }

        private void RaisePageChangedCancelled()
        {
            if (this.PageChangedCancelled != null)
            {
                this.PageChangedCancelled(this, EventArgs.Empty);
            }
        }

        private bool RaisePageChanging(int newIndex)
        {
            var args = new PageChangingEventArgs(newIndex);
            if (this.PageChanging != null)
            {
                this.PageChanging(this, args);
                if (args.Cancel)
                {
                    return false;
                }
            }
            return true;
        }

        #endregion

        public class PagedCollectionRequest
        {
            #region Constants and Fields

            private readonly Action<IEnumerable<T>, Exception, bool> callback;

            #endregion

            #region Constructors and Destructors

            public PagedCollectionRequest(int skip, int pageSize, PagedCollection<T> collection, Action<IEnumerable<T>, Exception, bool> callback)
            {
                if (callback == null)
                {
                    throw new ArgumentNullException("callback");
                }
                if (collection == null)
                {
                    throw new ArgumentNullException("collection");
                }

                this.Collection = collection;
                this.PageSize = pageSize;
                this.Skip = skip;
                this.callback = callback;
            }

            #endregion

            #region Public Properties

            public PagedCollection<T> Collection { get; private set; }

            public int PageSize { get; private set; }

            public int Skip { get; private set; }

            public object UserState { get; set; }

            #endregion

            #region Public Methods

            public void PostCancelled()
            {
                this.callback(null, null, true);
            }

            public void PostError(Exception exception)
            {
                this.callback(null, exception, false);
            }

            public void PostResult(IEnumerable<T> result)
            {
                this.callback(result, null, false);
            }

            #endregion
        }
    }
}